<div id="attrs" class="attrs">
    <div class="header">Enter new values and click the "Set" buttons</div>
    <div class="body">
        <ul class="hints">
            <li>Try entering valid and invalid values for x, y; or values which attempt to position the box outside it's parent (parent box co-ordinates are displayed next to the text box).</li>
            <li>Try entering rgb, hex or keyword color values [ <code>rgb(255,0,0)</code>, <code>#ff0000</code>, <code>red</code> ].</li>
        </ul>
        <div class="fields">
            <p>
                <form action="#"  id="setX" class="action">
                    <label for="x">x:</label>
                    <input type="text" name="x" id="x" />
                    <button type="submit">Set</button>
                    <span id="xhint" class="hint"></span>
                </form>
            </p>
            <p>
                <form action="#" id="setY" class="action">
                    <label for="y">y:</label>
                    <input type="text" name="y" id="y" />
                    <button type="submit">Set</button>
                    <span id="yhint" class="hint"></span>
                </form>
            </p>
            <p>
                <form action="#" id="setColor" class="action">
                    <label for="color">color:</label>
                    <input type="text" name="color" id="color" />
                    <button type="submit">Set</button>
                </form>
            </p>
        </div>
    </div>
    <div class="footer">
        <button type="button" class="action" id="setXY">Set XY</button>
        <button type="button" class="action" id="setAll">Set All</button>
        <button type="button" class="action" id="getAll">Get All</button>
    </div>
</div>

<div id="boxParent"></div>

<script type="text/javascript">
// Get a new YUI instance
YUI().use("node", "attribute", function(Y) {

    var boxParent = Y.one("#boxParent");

    // Setup a custom class with attribute support
    function Box(cfg) {
        this._createNode(cfg);

        // Attribute configuration
        var attrs = {

            "parent" : {
                value: null
            },

            "x" : {
                setter: function(val, name) {
                    // Pass through x value to xy
                    this.set("xy", [val, this.get("y")]);
                },

                getter: function(val, name) {
                    // Get x value from xy
                    return this.get("xy")[0];
                }
            },

            "y" : {
                setter: function(val, name) {
                    // Pass through y value to xy
                    this.set("xy", [this.get("x"), val]);
                },

                getter: function() {
                    // Get y value from xy
                    return this.get("xy")[1];
                }
            },

            "xy" : {
                // Actual stored xy co-ordinates
                value: [0, 0],

                setter: function(val, name) {
                    // Constrain XY value to the parent element.

                    // Returns the constrained xy value, which will
                    // be the final value stored.
                    return this.constrain(val);
                },

                validator: function(val, name) {
                    // Ensure we only store a valid data value
                    return (Y.Lang.isArray(val) && val.length == 2 && Y.Lang.isNumber(val[0]) && Y.Lang.isNumber(val[1]));
                }
            },

            "color" : {
                value: "olive",

                getter: function(val, name) {
                    // Always normalize the returned value to
                    // a hex color value,  even if the stored
                    // value is a keyword, or an rgb value.
                    if (val) {
                        return Y.Color.toHex(val);
                    } else {
                        return null;
                    }
                },

                validator: function(val, name) {
                    // Ensure we only store rgb, hex or keyword values.
                    return (Y.Color.re_RGB.test(val) || Y.Color.re_hex.test(val) || Y.Color.KEYWORDS[val]);
                }
            }
        };

        this.addAttrs(attrs, cfg);

        this._sync();
        this._bind();
    }

    Box.BUFFER = 5;

    // Create the box node
    Box.prototype._createNode = function() {
        this._node = Y.Node.create('<div class="yui3-box"><p>Positioned Box</p><p class="coord"></p><p class="color">None</p></div>');
    };

    // Update rendered state to match the attribute state
    Box.prototype._sync = function() {
        this._syncParent();
        this._syncXY();
        this._syncColor();
    };

    Box.prototype._syncParent = function() {
        this.get("parent").appendChild(this._node);
    };

    Box.prototype._syncXY = function() {
        this._node.setXY(this.get("xy"));
        this._node.one("p.coord").set("innerHTML", "[" + this.get("x") + "," + this.get("y") + "]");
    };

    Box.prototype._syncColor = function() {
        this._node.setStyle("backgroundColor", this.get("color"));
        this._node.one("p.color").set("innerHTML", this.get("color"));
    };

    // Bind listeners for attribute change events
    Box.prototype._bind = function() {

        // Reflect any changes in xy, to the rendered Node
        this.after("xyChange", this._syncXY);

        // Reflect any changes in color, to the rendered Node
        // and output the color value received from get
        this.after("colorChange", this._syncColor);

        // Append the rendered node to the parent provided
        this.after("parentChange", this._syncParent);

    };

    // Get min, max unconstrained values for X.
    Box.prototype.getXConstraints = function() {

        var parentRegion = this.get("parent").get("region"),
            nodeRegion = this._node.get("region"),
            nodeWidth = nodeRegion.right-nodeRegion.left;

        // Ceil/Floor to account for browsers which have sub-pixel values.

        return [Math.ceil(parentRegion.left + Box.BUFFER), Math.floor(parentRegion.right - nodeWidth - Box.BUFFER)];
    };

    // Get min, max unconstrained values for Y.
    Box.prototype.getYConstraints = function() {

        var parentRegion = this.get("parent").get("region"),
            nodeRegion = this._node.get("region"),
            nodeHeight = nodeRegion.bottom-nodeRegion.top;

        // Ceil/Floor to account for browsers which have sub-pixel values.

        return [Math.ceil(parentRegion.top + Box.BUFFER), Math.floor(parentRegion.bottom - nodeHeight - Box.BUFFER)];
    };

    // Constrain the x,y value provided
    Box.prototype.constrain = function(val) {

        // If the X value places the box outside it's parent,
        // modify it's value to place the box inside it's parent.

        var xConstraints = this.getXConstraints();

        if (val[0] < xConstraints[0]) {
            val[0] = xConstraints[0];
        } else {
            if (val[0] > xConstraints[1]) {
                val[0] = xConstraints[1];
            }
        }

        // If the Y value places the box outside it's parent,
        // modify it's value to place the box inside it's parent.

        var yConstraints = this.getYConstraints();

        if (val[1] < yConstraints[0]) {
            val[1] = yConstraints[0];
        } else {
            if (val[1] > yConstraints[1]) {
                val[1] = yConstraints[1];
            }
        }
        return val;
    };


    Y.augment(Box, Y.Attribute);

    // ------

    // Create a new instance of Box
    var box = new Box({
        parent : boxParent
    });

    // Set references to form controls
    var xTxt = Y.one("#x");
    var yTxt = Y.one("#y");
    var colorTxt = Y.one("#color");

    var xHint = Y.one("#xhint");
    var yHint = Y.one("#yhint");

    function getAll() {
        xTxt.set("value", box.get("x"));
        yTxt.set("value", box.get("y"));
        colorTxt.set("value", box.get("color"));
    }

    // Use event delegation for the action button clicks, and form submissions
    Y.delegate("click", function(e) {

        // Get Node target from the event object

        // We already know it's a button which has an action because
        // of our selector (button.action), so all we need to do is
        // route it based on the id.
        var id = e.currentTarget.get("id");

        switch (id) {
            case "setXY":
                box.set("xy", [parseInt(xTxt.get("value"), 10), parseInt(yTxt.get("value"), 10)]);
                break;
            case "setAll":
                box.set("xy", [parseInt(xTxt.get("value"), 10), parseInt(yTxt.get("value"), 10)]);
                box.set("color", Y.Lang.trim(colorTxt.get("value")));
                break;
            case "getAll":
                getAll();
                break;
            default:
                break;
        }

    }, "#attrs", "button.action");

    Y.all("#attrs form.action").on("submit", function(e) {

        e.preventDefault();

        // Get Node target from the event object

        // We already know it's a button which has an action because
        // of our selector (button.action), so all we need to do is
        // route it based on the id.
        var id = e.currentTarget.get("id");

        switch (id) {
            case "setX":
                box.set("x", parseInt(xTxt.get("value"), 10));
                break;
            case "setY":
                box.set("y", parseInt(yTxt.get("value"), 10));
                break;
            case "setColor":
                box.set("color", Y.Lang.trim(colorTxt.get("value")));
                break;
            default:
                break;
        }
    });

    // Bind listeners to provide min, max unconstrained value hints for x, y
    // (focus/blur doesn't bubble, so bind individual listeners)
    Y.on("focus", function() {
        var minmax = box.getXConstraints();
        xHint.set("innerHTML", "Valid values: " + minmax[0] + " to " + minmax[1]);
    }, xTxt);

    Y.on("focus", function() {
        var minmax = box.getYConstraints();
        yHint.set("innerHTML", "Valid values: " + minmax[0] + " to " + minmax[1]);
    }, yTxt);

    Y.on("blur", function() {
        xHint.set("innerHTML", "");
    }, xTxt);

    Y.on("blur", function() {
        yHint.set("innerHTML", "");
    }, yTxt);

    getAll();
});
</script>
